﻿using System;
using System.Collections.Generic;
using Business.Entities;
using System.Diagnostics;
using Business.Entities.Actions;

namespace Business.Logic
{
    /// <summary>
    /// Performs the actual sync logic
    /// </summary>
    public class Syncer
    {
        public enum SyncDirection
        {
            GoogleToOutlook,
            OutlookToGoogle
        }

        public enum SyncAction
        {
            DeleteTask,
            UpdateTask,
            CreateTask,
            MoveTask,
            ReorderTask,
            DeleteList,
            RenameList,
            CreateList,
            TaskReordering,
            DeleteDuplicate,
            UniqifyDuplicate
        }

        public bool DeleteEmptyListsFromGoogle = true;
        private IOutlookApi _outlook;
        private GoogleApi _google;
        public IProgressIndicator ProgressIndicator;
        public SyncErrorPackage ErrorPackage;

        public Syncer(GoogleApi googleApi, IOutlookApi outlookApi)
        {
            _outlook = outlookApi;
            _google = googleApi;
        }

        private void TreatDuplicates(TasksRoot oRoot)
        {
            var flatList = oRoot.GetAllTasks();
            var dic = new Dictionary<string, Task>();
            foreach (Task task in flatList)
            {
                if (String.IsNullOrEmpty(task.id)) continue;
                if (dic.ContainsKey(task.id))
                {
                    var dup = dic[task.id];
                    if (dup.name == task.name)
                    {
                        // Equal id and equal text: delete one of them
                        LogAction(SyncDirection.GoogleToOutlook, SyncAction.DeleteDuplicate, dup.name, new SyncReason("Task duplicate in outlook - deleting one of the copies"));
                        _outlook.DeleteTask(dup.OutlookTask);
                    }
                    else
                    {
                        // Equal id but different text: keep both, uniquify Id
                        LogAction(SyncDirection.GoogleToOutlook, SyncAction.UniqifyDuplicate, dup.name, new SyncReason("Task with duplicate ID in outlook - changing ID"));
                        task.SetNewId();
                        _outlook.UpdateTask(task, null);
                    }
                }
                else dic.Add(task.id, task);
            }
        }

        /// <summary>
        /// Performs synchronization between the two lists
        /// </summary>
        /// <param name="googleRoot"></param>
        /// <param name="outlookRoot"></param>
        public void Sync(TasksRoot googleRoot, TasksRoot outlookRoot)
        {
            // Initiate error logging
            ErrorPackage = new SyncErrorPackage();
            ErrorPackage.GoogleTasks = googleRoot;
            ErrorPackage.OutlookTasks = outlookRoot;
            try
            {
                SyncBody(googleRoot, outlookRoot);
                ErrorPackage = null; // No exception occured
            }
            catch (Exception ex)
            {
                ErrorPackage.ExceptionMessage = ex.Message;
                ErrorPackage.ExceptionStackTrace = ex.StackTrace;
                throw;
            }
        }

        /// <summary>
        /// The sync logic
        /// </summary>
        private void SyncBody(TasksRoot googleRoot, TasksRoot outlookRoot)
        {
            Trace.WriteLine("Google: DisableMultiple: " + _google.DisableMultipleLists + " ListName: " + _google.SingleListName);
            Trace.WriteLine("Outlook: " + _outlook); // Tostring method of outlookapi writes the same info
            // Get sync history
            SyncHistory syncHist = SyncHistory.GetInstance();
            _google.Transactional = true;
            // Iterate outlook, match against google
            var newGoogleTasks = new Dictionary<Task, Task>();
            var oLastMod = outlookRoot.GetLastModified();
            if (syncHist.OutlookLastModified > oLastMod) oLastMod = syncHist.OutlookLastModified;
            var gLastMod = googleRoot.GetLastModified();

            // 0.5 Check for tasks with duplicate key in outlook (bug in 2003/2007-version)
            TreatDuplicates(outlookRoot);

            // 1. Traverse outlook task and sync to google
            if (ProgressIndicator != null) ProgressIndicator.NextMinorStep();
            foreach (TaskList oList in outlookRoot.lists)
            {
                foreach (Task oTask in oList.SortedByOrdinal().child_entity)
                {
                    // List fetching must be inside inner loop since the googleRoot's list of lists can change during processing
                    TaskList gCorrespondingList = googleRoot.GetListByName(oList.name);
                    TaskList gList;
                    Task gTask = googleRoot.FindTask(oTask.id, out gList);
                    
                    if (gTask != null)
                    {
                        // Task exists. sync group and task
                        SyncTask(oTask, gTask, oList, gCorrespondingList, gList, googleRoot);
                    }
                    else
                    {
                        // Task deleted from google? we know if it has a google id - ie previously synced
                        if (oTask.HasGoogleId)
                        {
                            //Trace.WriteLine(String.Format("G->O: Delete task '{0}'", oTask.name));
                            LogAction(SyncDirection.GoogleToOutlook, SyncAction.DeleteTask, oTask.name, new SyncReason("Missing on Google, but has been synced with Google previously."));
                            _outlook.DeleteTask(oTask.OutlookTask);
                        }
                        else
                        {
                            if (gCorrespondingList == null)
                            {
                                // Create missing list
                                //Trace.WriteLine(String.Format("O->G: Create list '{0}'", oList.name));
                                LogAction(SyncDirection.OutlookToGoogle, SyncAction.CreateList, oList.name, new SyncReason("No list with that name on Google"));
                                gCorrespondingList = new TaskList(oList.name);
                                googleRoot.lists.Add(gCorrespondingList); // Keeping the google root updated 
                                _google.CreateList(gCorrespondingList);
                                _google.CommitTransaction(); // Need the list id
                            }
                            // Create missing task
                            //Trace.WriteLine(String.Format("O->G: Create task '{0}'", oTask.name));
                            LogAction(SyncDirection.OutlookToGoogle, SyncAction.CreateTask, oTask.name, new SyncReason("Task not existing on Google"));
                            var clone = oTask.Clone(); 
                            clone.child_id = null; // Google insert does not allow child ids. Order sync will take care of this
                            clone.OutlookTask = oTask.OutlookTask;;
                            gCorrespondingList.Add(clone); // Keeping the google root updated
                            _google.CreateTask(gCorrespondingList, clone);
                            newGoogleTasks.Add(oTask, clone);
                        }
                    }
                }
            }
            // Commit transaction
            _google.CommitTransaction();

            // Write id's and child id's of newly created gogle tasks to the corresponding outlook tasks
            foreach (KeyValuePair<Task,Task> elem in newGoogleTasks)
            {
                Task oTask = elem.Key;
                Task gTask = elem.Value;
                string newId = gTask.id;
                oTask.SetNewId(gTask.id);
                oTask.ParentList.id = gTask.ListId;
                oTask.ListId = gTask.ListId;
                // Temp:
                Trace.WriteLine("  O: Storing new ID: " + newId);
                _outlook.SetGoogleId(oTask, true);
                if (oTask.ParentTask != null) _outlook.UpdateTask(oTask.ParentTask, null);
                // Update outlookRoot
            }

            // 1.5 make we set the correct parent of all tasks in google to avoid cascading deletes
            if (oLastMod > gLastMod)
            {
                foreach (TaskList gList in googleRoot.lists)
                {
                    var oList = outlookRoot.GetListByName(gList.name);
                    if (oList != null) SyncParents(gList, oList);
                }
            }

            if (ProgressIndicator != null) ProgressIndicator.NextMinorStep();
            // 2. Sync the google tasks with the outlook tasks (new and deleted)
            var outlookGoogleIds = _outlook.GetGoogleIds(); // List of google ids stored in outlook
            //Trace.WriteLine(  "Contents of OutlookGoogleId's: " + Environment.NewLine + outlookGoogleIds.Dump());
            var deltedTasks = new List<Task>();
            foreach (TaskList gList in googleRoot.lists)
            {
                foreach (Task gTask in gList.child_entity)
                {
                    //if (!oTasksByGoogleId.ContainsKey(gTask.id))
                    //if (!_outlook.ExistsGoogleTask(gTask.id))
                    if (!outlookGoogleIds.Contains(gTask.id))
                    {
                        if (syncHist.GoogleIds.Contains(gTask.id))
                        {
                            // Existed in outlook at last sync - indicates user has deleted it from outlook
                            //Trace.WriteLine("O->G: Delete '" + gTask + "'");
                            LogAction(SyncDirection.OutlookToGoogle, SyncAction.DeleteTask, gTask.ToString(), new SyncReason("Existed in Outlook on last sync"));
                            _google.DeleteTask(gTask);
                            deltedTasks.Add(gTask);
                        }
                        else if (gTask.name.Trim() == String.Empty)
                        {
                            deltedTasks.Add(gTask); // Don't create the empty tasks (but they're not really deleted)
                        }
                        else
                        {
                            //Trace.WriteLine("G->O: Create '" + gTask + "'");
                            LogAction(SyncDirection.GoogleToOutlook, SyncAction.CreateTask, gTask.ToString(), new SyncReason("Exists on Google, not in Outlook"));
                            _outlook.InsertTask(gTask, gList.name);
                        }
                    }
                }
            }
            foreach (Task task in deltedTasks) task.ParentList.Remove(task); // Keeping the google root updated 

            // 2.5 Delete empty list from google?
            DeleteIfEmptyAndConfigured(googleRoot);

            if (ProgressIndicator != null) ProgressIndicator.NextMinorStep();
            // 3 Sync the order the task
            outlookRoot = _outlook.GetOutlookTasks(); // Refetching the list, as elements may have change
            
            if (gLastMod > oLastMod)
            {
                LogAction(SyncDirection.GoogleToOutlook, SyncAction.TaskReordering, null, new SyncReason(gLastMod, oLastMod));
                foreach (TaskList gList in googleRoot.lists)
                {
                    foreach (Task gTask in gList.child_entity)
                    {
                        _outlook.SetOrdinalByCachedGoogleId(gTask.id, gTask.Ordinal, gTask.child_id);
                    }
                }
            }
            else
            {
                LogAction(SyncDirection.OutlookToGoogle, SyncAction.TaskReordering, null, new SyncReason(gLastMod, oLastMod));
                OrderGoogleTasksFromOutlook(googleRoot, outlookRoot);
            }

            if (ProgressIndicator != null && _google.Transaction.action_list.Count > 0) ProgressIndicator.NextMinorStep();
            // Commit transaction
            _google.CommitTransaction();

            if (ProgressIndicator != null) ProgressIndicator.NextMinorStep();

            // Write id's of newly created gogle tasks to the corresponding outlook tasks
            //foreach (Task task in newGoogleTasks) _outlook.SetGoogleId(task);
            
            // (Outlook Mobile) Set snapshot
            foreach (TaskList oList in outlookRoot.lists)
            {
                foreach (Task oTask in oList.child_entity)
                {
                    _outlook.SetSnapshot(oTask, true);
                }
            }

            // Save outlook ids to disk so that we can differ between user deleted outlook task and created gooogle task
            SaveSyncHistory();
        }


        /// <summary>
        /// Traces and logs a sync event
        /// </summary>
        /// <param name="direction">Google to outlook or vice versa</param>
        /// <param name="action">Move, delete etc</param>
        /// <param name="target">Task or tasklist description</param>
        /// <param name="reason">Reason object or null</param>
        private void LogAction(SyncDirection direction, SyncAction action, string target, SyncReason reason)
        {
            string actionText = null;
            switch (action)
            {
                case SyncAction.TaskReordering: actionText = "Performing task reordering"; break;
                case SyncAction.CreateList: actionText = "Create list"; break;
                case SyncAction.CreateTask: actionText = "Create task"; break;
                case SyncAction.DeleteList: actionText = "Delete list"; break;
                case SyncAction.DeleteTask: actionText = "Delete task"; break;
                case SyncAction.MoveTask: actionText = "Move task"; break;
                case SyncAction.ReorderTask: actionText = "Reordering task"; break;
                case SyncAction.RenameList: actionText = "Rename list"; break;
                case SyncAction.UpdateTask: actionText = "Update task"; break;
                default: actionText = action.ToString(); break;
            }
            string dirText = direction == SyncDirection.GoogleToOutlook ? "G>O": "O>G";
            string traceText;
            if (reason == null) traceText = String.Format("{0}: {1} {2}", dirText, actionText, target);
            else                traceText = String.Format("{0}: {1} {2} ({3})", dirText, actionText, target, reason);
            Trace.WriteLine(traceText);
        }
            
        /// <summary>
        /// Syncs the two tasks
        /// </summary>
        private void SyncTask(Task oTask, Task gTask, TaskList oList, TaskList gCorrespondingList, TaskList gList, TasksRoot googleRoot)
        {
            bool dataEquals = oTask.DataEquals(gTask);
            // We use Max(List.LastMod, Task.LastMod) to compare with the outlook task to work around
            // issue with list renaming in google which only touches the list entity which has no corresponding entity in outlook
            var googleLastMod = gTask.LastModified;
            if (gList.LastModified > googleLastMod) googleLastMod = gList.LastModified;
            if (oTask.LastModified > googleLastMod)
            {
                // Same list? if not, new list or move item?
                if (!gList.Equals(gCorrespondingList))
                {
                    if (gCorrespondingList == null)
                    {
                        //Trace.WriteLine(String.Format("O->G: Create list '{0}'", oList.name));
                        LogAction(SyncDirection.OutlookToGoogle, SyncAction.CreateList, oList.NameSafe, new SyncReason("Category does not exist as list on Google"));
                        gCorrespondingList = new TaskList(oList.NameSafe);
                        googleRoot.Add(gCorrespondingList); // Keeping the google root updated 
                        _google.CreateList(gCorrespondingList);
                        _google.CommitTransaction(); // Need the list id
                    }
                    //Trace.WriteLine(String.Format("O->G: Moving task '{0}' from '{1}' to '{2}'", oTask.name, gList.name, gCorrespondingList.name));
                    var targetString = String.Format("'{0}' from '{1}' to '{2}'", oTask.name, gList.name, gCorrespondingList.name);
                    LogAction(SyncDirection.OutlookToGoogle, SyncAction.MoveTask, targetString, new SyncReason(googleLastMod, oTask.LastModified));
                    _google.MoveTask(gTask, gList, gCorrespondingList);
                    googleRoot.MoveTask(gTask, gList, gCorrespondingList); // Keeping the google root updated
                }
                // Only update the task itself if core attributes are changed
                if (!dataEquals)
                {
                    //Trace.WriteLine("O->G: Updating " + oTask);
                    LogAction(SyncDirection.OutlookToGoogle, SyncAction.UpdateTask, oTask.ToString(), new SyncReason(googleLastMod, oTask.LastModified));
                    gTask.CopyProperties(oTask, false);
                    _google.UpdateTask(gTask);
                }
            }
            else
            {
                // Update if core properties or list is changed
                if (!dataEquals || !gList.Equals(gCorrespondingList))
                {
                    // Lists easier on outlook: just set the category right
                    //Trace.WriteLine("G->O: Updating" + oTask);
                    LogAction(SyncDirection.GoogleToOutlook, SyncAction.UpdateTask, gTask.ToString(), new SyncReason(googleLastMod, oTask.LastModified));
                    oTask.CopyProperties(gTask, false);
                    oTask.LastModified = gTask.LastModified.AddSeconds(-1); // Workaround for POOM not storing lastmod.
                    var listNameOrNull = gList.Equals(gCorrespondingList) ? null : gList.name ; 
                    // Dont update list name if it's equal. Sounds irrellevant, but matters when you deal with multiple categories in outlook
                    _outlook.UpdateTask(oTask, listNameOrNull);
                }
            }
        }


        /// <summary>
        /// Deletes all empty google lists if the program is configured to do so
        /// </summary>
        /// <param name="root"></param>
        private void DeleteIfEmptyAndConfigured(TasksRoot root)
        {
            if (!DeleteEmptyListsFromGoogle) return;
            TaskList[] listCopy = new TaskList[root.lists.Count];
            root.lists.CopyTo(listCopy);
            foreach (TaskList gList in listCopy)
            {
                if (gList.child_entity.Count == 0)
                {
                    //Trace.WriteLine("O->G: Deleting list " + gList);
                    LogAction(SyncDirection.OutlookToGoogle, SyncAction.DeleteList, gList.NameSafe, new SyncReason("Last task with this category deleted from Outlook"));
                    _google.DeleteList(gList);
                    root.lists.Remove(gList);
                }
            }
        }

        /// <summary>
        /// Orders the google tasks based on outlook ordinal
        /// </summary>
        /// <param name="googleRoot"></param>
        /// <param name="outlookRoot"></param>
        private void OrderGoogleTasksFromOutlook(TasksRoot googleRoot, TasksRoot outlookRoot)
        {
            foreach (var gList in googleRoot.lists)
            {
                if (gList.child_entity.Count == 0) continue; // Empty list is not an enitity in Outlook
                var oList = outlookRoot.GetListByName(gList.name);
                if (oList == null) throw new ArgumentException("OrderGoogleTask: Lists not synced");
                var sgList = gList.SortedByOrdinal();
                var soList = oList.SortedByOrdinal();
                bool anyMoved = false;
                do
                {
                    anyMoved = MoveUnorderedElement(sgList, soList);
                } while (anyMoved);
            }
            _google.CommitTransaction();
        }

        private bool MoveUnorderedElement(TaskList sgList, TaskList soList)
        {
            if (sgList.child_entity.Count != soList.child_entity.Count) 
                throw new ArgumentException("MoveUnorderedElement: lists not of equal length");
            Task priorTask = null;
            Task gTask, oTask;
            for (int i = 0; i < sgList.child_entity.Count; i++)
            {
                gTask = sgList.child_entity[i];
                oTask = soList.child_entity[i];
                bool parentEquals = gTask.id == oTask.id && oTask.ParentElementId == gTask.ParentElementId;
                    //Task.ParentTaskEquals(oTask, gTask);
                if (gTask.id != oTask.id)
                {
                    var correctElemHere = sgList.GetTask(soList.child_entity[i].id);

                    priorTask = oTask.GetPriorTask(); 
                    //Trace.WriteLine(String.Format("  Moving Google Task '{0}', prior id {1} ", correctElemHere, priorTask == null ? "null" : priorTask.ToString()));
                    string moveText = String.Format("'{0}', prior id {1} ", correctElemHere, priorTask == null ? "null" : priorTask.ToString());
                    LogAction(SyncDirection.OutlookToGoogle, SyncAction.ReorderTask, moveText, new SyncReason("Order differ"));

                    _google.MoveTaskInternally(correctElemHere, priorTask);
                    
                    sgList.child_entity.Remove(correctElemHere);
                    sgList.child_entity.Insert(i, correctElemHere);
                    return true; // Restart, we have altered the order of the list
                }
                else if (!parentEquals)
                {
                    gTask.ParentList.SetTaskParent(gTask, oTask.ParentElementId);
                    priorTask = oTask.GetPriorTask();
                    //Trace.WriteLine(String.Format("  Moving Google Task '{0}', new parent {1}, prior: {2} ", oTask.id, oTask.ParentElementId, priorTask == null ? "null" : priorTask.ToString()));
                    string moveText = String.Format("'{0}', new parent {1}, prior: {2} ", oTask.id, oTask.ParentElementId, priorTask == null ? "null" : priorTask.ToString());
                    LogAction(SyncDirection.OutlookToGoogle, SyncAction.MoveTask, moveText, new SyncReason("Order differ"));

                    _google.MoveTaskInternally(gTask, priorTask);
                }
                //priorTask = gTask;
            }
            return false;
        }

        /// <summary>
        /// Makes sure the google parent is identical to the outlook parent.
        /// Need to sync the parents so that we dont cascade deletes of higher level objects
        /// </summary>
        /// <param name="gList"></param>
        /// <param name="oList"></param>
        private void SyncParents(TaskList gList, TaskList oList)
        {
            foreach (Task oTask in oList.child_entity)
            {
                var gTask = gList.GetTask(oTask.id);
                if (gTask != null)
                {
                    bool parentEquals = gTask.id == oTask.id && oTask.ParentElementId == gTask.ParentElementId;
                    if (!parentEquals)
                    {
                        gTask.ParentList.SetTaskParent(gTask, oTask.ParentElementId);
                        var priorTask = oTask.GetPriorTask();
                        //Trace.WriteLine(String.Format("  Moving* Google Task '{0}', new parent {1}, prior: {2} ", oTask.id,oTask.ParentElementId,priorTask == null ? "null" : priorTask.ToString()));
                        string moveText = String.Format("'{0}', new parent {1}, prior: {2} ", oTask.id, oTask.ParentElementId, priorTask == null ? "null" : priorTask.ToString());
                        LogAction(SyncDirection.OutlookToGoogle, SyncAction.ReorderTask, moveText, new SyncReason("Parent task differ"));
                        _google.MoveTaskInternally(gTask, priorTask);
                    }
                }
            }
        }

        private void SaveSyncHistory()
        {
            var oRoot = _outlook.GetOutlookTasks();
            SyncHistory sh = SyncHistory.GetInstance();
            sh.SetData(oRoot);
            sh.Save();
        }
    }
}
